0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🎮 JavaScriptで作る!WEB制作会社経営シミュレーションゲーム開発入門 💻

Posted at

ブラウザで動く経営シミュレーションゲームの作り方 🎮

こんにちは、@YushiYamamotoです!今回は「ブラウザで動作する経営シミュレーションゲーム」の作り方について、初心者の方にも分かりやすく解説していきます。JavaScriptを使って、シンプルながらも奥深いゲーム開発の世界に飛び込んでみましょう!

See the Pen ラク子のWEB制作会社 by Yushi Yamamoto (@yamamotoyushi) on CodePen.

🌟 なぜ経営シミュレーションゲーム?

経営シミュレーションゲームは、プレイヤーが経営者となって会社やお店を運営するゲームです。資金管理、スタッフ雇用、設備投資など、現実の経営要素を取り入れつつ、ゲームとして楽しめる要素が満載です。

また、技術的な観点からも:

  • オブジェクト指向プログラミングの練習に最適
  • 状態管理の仕組みを学べる
  • UIとロジックの分離を実践できる

📋 開発の流れ

ゲーム開発の基本的な流れは以下のようになります:

今回は、「WEB制作会社経営シミュレーション」というテーマで進めていきます。プレイヤーはWEB制作会社の社長となり、クライアントから依頼を受けてWebサイトを制作し、会社を成長させていくゲームです。

💻 開発環境の準備

ブラウザゲーム開発に必要なものは非常にシンプルです:

  • テキストエディタ(VSCode推奨)
  • Webブラウザ(Chrome推奨)
  • 基本的なHTML/CSS/JavaScript知識

特別なゲームエンジンは使わず、純粋なJavaScriptで実装していきます。これにより、基本的なプログラミングの概念を深く理解できます。

🏗️ ゲームの基本設計

まずは、ゲームの基本構造を考えましょう。経営シミュレーションゲームの核となる要素は以下の通りです:

  1. 会社の状態管理:資金、評判、所有設備など
  2. 時間の経過:日付進行システム
  3. プロジェクト管理:依頼の受注、進行、完了
  4. スタッフ管理:雇用、スキル成長、給与支払い
  5. 設備投資:機材購入、オフィス拡張

これらの要素をクラス図で表すと次のようになります:

+------------------+       +------------------+
|      Game        |       |     Project      |
+------------------+       +------------------+
| - company        || - id             |
| - gameTime       |       | - name           |
| - advanceDay()   |       | - budget         |
| - saveGame()     |       | - deadline       |
+------------------+       | - assignedStaff  |
         |                 +------------------+
         |
         v
+------------------+       +------------------+
|     Company      ||      Staff       |
+------------------+       +------------------+
| - name           |       | - id             |
| - funds          |       | - name           |
| - reputation     |       | - skills         |
| - staff          |       | - salary         |
| - projects       |       | - motivation     |
| - equipment      |       +------------------+
+------------------+

🚀 基本システムの実装

それでは、実際にコードを書いていきましょう。まずは、ゲームの基本クラスを作成します。

JS
// ゲームのメインクラス
class WebDevTycoon {
  constructor() {
    // 会社の初期状態
    this.company = {
      name: "マイWEB制作会社",
      funds: 500000,  // 初期資金:50万円
      reputation: 50, // 評判:0-100
      staff: [],      // スタッフリスト
      projects: [],   // プロジェクトリスト
      equipment: [],  // 所有機材リスト
      office: {size: "small", rent: 50000} // オフィス情報
    };
    
    // ゲーム内時間
    this.gameTime = {
      day: 1,
      month: 4,
      year: 2025
    };
    
    // 初期スタッフ(プレイヤー自身)を追加
    this.company.staff.push({
      id: 1,
      name: "あなた",
      role: "社長",
      salary: 0,  // 社長なので給料なし
      skills: {
        design: 50,
        coding: 50,
        management: 50,
        communication: 60
      },
      motivation: 100
    });
  }
  
  // 日付を進める
  advanceDay() {
    this.gameTime.day++;
    
    // 月末処理
    if (this.gameTime.day > 30) {
      this.gameTime.day = 1;
      this.gameTime.month++;
      this.payMonthlyExpenses(); // 月次経費支払い
    }
    
    // 年末処理
    if (this.gameTime.month > 12) {
      this.gameTime.month = 1;
      this.gameTime.year++;
      this.annualReport(); // 年次レポート
    }
    
    // プロジェクトの進行
    this.updateProjects();
    
    // ランダムイベント発生
    this.generateRandomEvents();
  }
  
  // 月次経費の支払い
  payMonthlyExpenses() {
    // オフィス賃料
    this.company.funds -= this.company.office.rent;
    
    // スタッフの給料
    for (const staff of this.company.staff) {
      if (staff.salary > 0) { // プレイヤー以外のスタッフ
        this.company.funds -= staff.salary;
      }
    }
    
    // 設備維持費
    const equipmentMaintenance = this.company.equipment.length * 5000;
    this.company.funds -= equipmentMaintenance;
    
    return {
      rent: this.company.office.rent,
      salaries: this.company.staff.reduce((sum, staff) => sum + staff.salary, 0),
      maintenance: equipmentMaintenance
    };
  }
  
  // プロジェクトの更新
  updateProjects() {
    for (const project of this.company.projects) {
      if (!project.completed) {
        project.daysLeft--;
        
        // プロジェクトが期限切れになった場合
        if (project.daysLeft  p.id === projectId);
    if (!project) return { success: false, message: "プロジェクトが見つかりません。" };
    
    // 品質計算
    const quality = this.calculateProjectQuality(project);
    
    // 報酬と評判の更新
    this.company.funds += project.remainingPayment;
    
    // 品質に応じた評判変動
    let reputationChange = 0;
    if (quality > 80) {
      reputationChange = 5;
    } else if (quality > 50) {
      reputationChange = 2;
    } else {
      reputationChange = -3;
    }
    
    this.company.reputation = Math.max(0, Math.min(100, this.company.reputation + reputationChange));
    
    // プロジェクト完了フラグを立てる
    project.completed = true;
    
    // スタッフのスキルアップ
    for (const staffId of project.assignedStaff) {
      const staff = this.company.staff.find(s => s.id === staffId);
      if (staff) {
        // プロジェクトの種類に応じたスキルアップ
        for (const skill of project.requiredSkills) {
          if (staff.skills[skill]) {
            staff.skills[skill] = Math.min(100, staff.skills[skill] + 1);
          }
        }
      }
    }
    
    return {
      success: true,
      message: `${project.name}が完了しました!品質: ${quality}点、報酬: ${project.remainingPayment.toLocaleString()}円`,
      quality: quality,
      payment: project.remainingPayment,
      reputationChange: reputationChange
    };
  }
  
  // プロジェクトの品質を計算
  calculateProjectQuality(project) {
    // 基本品質
    let quality = 50;
    
    // スタッフのスキル影響
    const assignedStaff = project.assignedStaff || [];
    for (const staffId of assignedStaff) {
      const staff = this.company.staff.find(s => s.id === staffId);
      if (staff) {
        // プロジェクトに必要なスキルの平均値を計算
        let skillSum = 0;
        let skillCount = 0;
        
        for (const skill of project.requiredSkills) {
          if (staff.skills[skill]) {
            skillSum += staff.skills[skill];
            skillCount++;
          }
        }
        
        if (skillCount > 0) {
          const avgSkill = skillSum / skillCount;
          quality += avgSkill / assignedStaff.length;
        }
        
        // モチベーションの影響
        quality += (staff.motivation - 50) / 10;
      }
    }
    
    // 納期影響
    if (project.daysLeft > 5) {
      quality += 10; // 余裕があると品質アップ
    } else if (project.daysLeft 
HTML

HTML

  <div class="game-container">
    <div class="game-header">
      <div class="company-info">
        <h1 class="company-name" id="company-name">ラク子のWEB制作会社</h1>
        <div class="company-stats">
          <div class="stat-item">
            <span class="stat-label">資金</span>
            <span class="stat-value" id="funds">500,000円</span>
          </div>
          <div class="stat-item">
            <span class="stat-label">評判</span>
            <span class="stat-value" id="reputation">50</span>
          </div>
        </div>
      </div>
      <div class="game-date" id="date">2025年4月1日</div>
    </div>
    
    <div class="rakuko-area">
      <img src="https://rakuraku-site.prodouga.com/img/RakuRaku.avif" alt="ラク子" class="rakuko-image normal" id="rakuko-image">
      <div class="rakuko-message" id="rakuko-message">今日も頑張りましょう!</div>
      <button class="next-day-button" id="next-day-button">次の日へ</button>
    </div>
    
    <div class="game-main">
      <div class="action-buttons">
        <button class="action-button" id="hire-staff-button">スタッフを雇用</button>
        <button class="action-button" id="buy-equipment-button">機材を購入</button>
        <button class="action-button" id="upgrade-office-button">オフィスをアップグレード</button>
      </div>
      
      <div class="game-content">
        <div class="content-section projects-section">
          <h2 class="section-title">プロジェクト</h2>
          <ul class="projects-list" id="projects-list">
            <li class="empty-list">現在進行中のプロジェクトはありません</li>
          </ul>
        </div>
        
        <div class="content-section staff-section">
          <h2 class="section-title">スタッフ</h2>
          <ul class="staff-list" id="staff-list"></ul>
        </div>
        
        <div class="content-section equipment-section">
          <h2 class="section-title">機材</h2>
          <ul class="equipment-list" id="equipment-list"></ul>
        </div>
      </div>
      
      <div class="message-log" id="message-log"></div>
    </div>
  </div>
CSS

CSS

    /* ゲーム全体のスタイル */
    body {
      font-family: 'Hiragino Kaku Gothic Pro', 'メイリオ', sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f5f5f5;
      color: #333;
    }
    
    .game-container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
      display: grid;
      grid-template-columns: 300px 1fr;
      grid-template-rows: auto 1fr;
      gap: 20px;
      height: 100vh;
    }
    
    /* ヘッダー部分 */
    .game-header {
      grid-column: 1 / 3;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    }
    
    .company-info {
      display: flex;
      flex-direction: column;
    }
    
    .company-name {
      font-size: 24px;
      font-weight: bold;
      margin: 0;
    }
    
    .company-stats {
      display: flex;
      gap: 20px;
    }
    
    .stat-item {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    .stat-label {
      font-size: 12px;
      color: #666;
    }
    
    .stat-value {
      font-size: 18px;
      font-weight: bold;
    }
    
    .game-date {
      padding: 5px 10px;
      background-color: #f0f0f0;
      border-radius: 4px;
      font-size: 14px;
    }
    
    /* ラク子エリア */
    .rakuko-area {
      grid-column: 1;
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    .rakuko-image {
      width: 200px;
      height: 200px;
      object-fit: cover;
      border-radius: 50%;
      margin-bottom: 15px;
      transition: all 0.3s ease;
    }
    
    .rakuko-image.happy {
      border: 5px solid #4CAF50;
    }
    
    .rakuko-image.normal {
      border: 5px solid #FFC107;
    }
    
    .rakuko-image.sad {
      border: 5px solid #F44336;
    }
    
    .rakuko-message {
      font-size: 16px;
      text-align: center;
      margin-bottom: 20px;
      min-height: 48px;
    }
    
    .next-day-button {
      padding: 10px 20px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .next-day-button:hover {
      background-color: #45a049;
    }
    
    /* ゲームメインエリア */
    .game-main {
      grid-column: 2;
      display: grid;
      grid-template-rows: auto 1fr;
      gap: 20px;
    }
    
    .action-buttons {
      display: flex;
      gap: 10px;
    }
    
    .action-button {
      padding: 10px 15px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 14px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .action-button:hover {
      background-color: #0b7dda;
    }
    
    .game-content {
      display: grid;
      grid-template-columns: 1fr 1fr;
      grid-template-rows: 1fr 1fr;
      gap: 20px;
    }
    
    .content-section {
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      overflow-y: auto;
    }
    
    .section-title {
      font-size: 18px;
      font-weight: bold;
      margin-top: 0;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 1px solid #eee;
    }
    
    /* プロジェクトリスト */
    .projects-section {
      grid-column: 1 / 3;
      grid-row: 1;
    }
    
    .projects-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .project-item {
      margin-bottom: 15px;
      border: 1px solid #eee;
      border-radius: 4px;
      overflow: hidden;
    }
    
    .project-header {
      padding: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .project-header.normal {
      background-color: #E3F2FD;
    }
    
    .project-header.urgent {
      background-color: #FFF3E0;
    }
    
    .project-header.overdue {
      background-color: #FFEBEE;
    }
    
    .project-header.completed {
      background-color: #E8F5E9;
    }
    
    .project-header h3 {
      margin: 0;
      font-size: 16px;
    }
    
    .project-client {
      font-size: 12px;
      color: #666;
    }
    
    .project-details {
      padding: 10px;
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
    }
    
    .project-details p {
      margin: 0;
      font-size: 14px;
    }
    
    .project-actions {
      padding: 10px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      background-color: #f9f9f9;
    }
    
    .project-actions button {
      padding: 5px 10px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .project-actions button:hover {
      background-color: #0b7dda;
    }
    
    .status-badge {
      padding: 5px 10px;
      border-radius: 4px;
      font-size: 12px;
    }
    
    .status-badge.completed {
      background-color: #4CAF50;
      color: white;
    }
    
    .empty-list {
      padding: 20px;
      text-align: center;
      color: #999;
    }
    
    /* スタッフリスト */
    .staff-section {
      grid-column: 1;
      grid-row: 2;
    }
    
    .staff-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .staff-item {
      margin-bottom: 15px;
      border: 1px solid #eee;
      border-radius: 4px;
      overflow: hidden;
    }
    
    .staff-header {
      padding: 10px;
      background-color: #E8EAF6;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .staff-header h3 {
      margin: 0;
      font-size: 16px;
    }
    
    .staff-role {
      font-size: 12px;
      color: #666;
    }
    
    .staff-details {
      padding: 10px;
    }
    
    .staff-details p {
      margin: 5px 0;
      font-size: 14px;
    }
    
    .staff-skills {
      margin: 10px 0;
    }
    
    .skill-bar {
      display: flex;
      align-items: center;
      margin-bottom: 5px;
    }
    
    .skill-name {
      width: 100px;
      font-size: 12px;
    }
    
    .skill-value-bar {
      flex-grow: 1;
      height: 8px;
      background-color: #eee;
      border-radius: 4px;
      overflow: hidden;
      margin: 0 10px;
    }
    
    .skill-value {
      height: 100%;
      background-color: #2196F3;
    }
    
    .skill-value-text {
      font-size: 12px;
      width: 30px;
      text-align: right;
    }
    
    .motivation {
      font-weight: bold;
    }
    
    .motivation-high {
      color: #4CAF50;
    }
    
    .motivation-medium {
      color: #FFC107;
    }
    
    .motivation-low {
      color: #F44336;
    }
    
    .staff-actions {
      padding: 10px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      background-color: #f9f9f9;
    }
    
    .staff-actions button {
      padding: 5px 10px;
      background-color: #673AB7;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .staff-actions button:hover {
      background-color: #5e35b1;
    }
    
    /* 機材リスト */
    .equipment-section {
      grid-column: 2;
      grid-row: 2;
    }
    
    .equipment-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .equipment-item {
      margin-bottom: 15px;
      border: 1px solid #eee;
      border-radius: 4px;
      overflow: hidden;
    }
    
    .equipment-header {
      padding: 10px;
      background-color: #E0F7FA;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .equipment-header h3 {
      margin: 0;
      font-size: 16px;
    }
    
    .equipment-details {
      padding: 10px;
    }
    
    .equipment-details p {
      margin: 5px 0;
      font-size: 14px;
    }
    
    .condition {
      font-weight: bold;
    }
    
    .condition-good {
      color: #4CAF50;
    }
    
    .condition-fair {
      color: #FFC107;
    }
    
    .condition-poor {
      color: #F44336;
    }
    
    .equipment-actions {
      padding: 10px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      background-color: #f9f9f9;
    }
    
    .equipment-actions button {
      padding: 5px 10px;
      background-color: #00BCD4;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .equipment-actions button:hover {
      background-color: #00acc1;
    }
    
    /* メッセージログ */
    .message-log {
      grid-column: 1 / 3;
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      max-height: 200px;
      overflow-y: auto;
    }
    
    .message {
      padding: 5px 10px;
      margin-bottom: 5px;
      border-radius: 4px;
      font-size: 14px;
    }
    
    .message.info {
      background-color: #E3F2FD;
    }
    
    .message.warning {
      background-color: #FFF3E0;
    }
    
    .message.error {
      background-color: #FFEBEE;
    }
    
    /* ダイアログ */
    .dialog {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 1000;
    }
    
    .dialog-content {
      background-color: #fff;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 5px 15px rgba(0,0,0,0.3);
      max-width: 80%;
      max-height: 80%;
      overflow-y: auto;
    }
    
    .dialog-content h2 {
      margin-top: 0;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 1px solid #eee;
    }
    
    .dialog-staff-list,
    .dialog-skill-list,
    .dialog-hire-list,
    .dialog-equipment-list,
    .dialog-office-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .dialog-staff-item,
    .dialog-skill-item,
    .dialog-hire-item,
    .dialog-equipment-item,
    .dialog-office-item {
      margin-bottom: 15px;
      padding: 10px;
      border: 1px solid #eee;
      border-radius: 4px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .dialog-staff-item.assigned {
      background-color: #E8F5E9;
    }
    
    .dialog-office-item.current {
      background-color: #E8F5E9;
    }
    
    .dialog-actions {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      margin-top: 20px;
    }
    
    .dialog-actions button {
      padding: 10px 20px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 14px;
      cursor: pointer;
    }
    
    .dialog-actions button:hover {
      background-color: #0b7dda;
    }
    
    .close-dialog-button {
      background-color: #9E9E9E !important;
    }
    
    .close-dialog-button:hover {
      background-color: #757575 !important;
    }
    
    /* レスポンシブデザイン */
    @media (max-width: 1024px) {
      .game-container {
        grid-template-columns: 1fr;
      }
      
      .game-header {
        grid-column: 1;
      }
      
      .rakuko-area {
        grid-column: 1;
      }
      
      .game-main {
        grid-column: 1;
      }
      
      .game-content {
        grid-template-columns: 1fr;
      }
      
      .projects-section {
        grid-column: 1;
      }
      
      .staff-section,
      .equipment-section {
        grid-column: 1;
      }
    }

🔄 ゲームループの実装

ゲームの動作を制御するために、HTMLとJavaScriptを連携させます。以下は、UIの更新とゲームの進行を管理するコードです。


// HTMLとの連携用コード
document.addEventListener('DOMContentLoaded', function() {
  // ゲームインスタンスの作成
  const game = new WebDevTycoon();
  
  // UI要素の取得
  const companyNameElement = document.getElementById('company-name');
  const fundsElement = document.getElementById('funds');
  const reputationElement = document.getElementById('reputation');
  const dateElement = document.getElementById('date');
  const characterMessageElement = document.getElementById('character-message');
  const projectsListElement = document.getElementById('projects-list');
  const staffListElement = document.getElementById('staff-list');
  const equipmentListElement = document.getElementById('equipment-list');
  const nextDayButton = document.getElementById('next-day-button');
  const messageLogElement = document.getElementById('message-log');
  
  // ゲーム状態の更新
  function updateUI() {
    // 基本情報の更新
    companyNameElement.textContent = game.company.name;
    fundsElement.textContent = game.company.funds.toLocaleString() + '';
    reputationElement.textContent = game.company.reputation;
    dateElement.textContent = `${game.gameTime.year}${game.gameTime.month}${game.gameTime.day}日`;
    
    // プロジェクトリストの更新
    updateProjectsList(game.company.projects);
    
    // スタッフリストの更新
    updateStaffList(game.company.staff);
    
    // 機材リストの更新
    updateEquipmentList(game.company.equipment);
  }
  
  // 次の日へボタンのイベントリスナー
  nextDayButton.addEventListener('click', function() {
    const result = game.advanceDay();
    addMessageToLog(`${game.gameTime.year}${game.gameTime.month}${game.gameTime.day}日になりました。`);
    
    // ランダムイベントの処理
    const event = game.generateRandomEvents();
    if (event) {
      addMessageToLog(event.message);
      
      if (event.type === 'inquiry') {
        showNewProjectDialog(event.project);
      }
    }
    
    updateUI();
  });
  
  // 初期UI更新
  updateUI();
  
  // 初期メッセージ
  addMessageToLog('ゲームを開始しました。WEB制作会社経営へようこそ!');
});

🧩 主要機能の実装

ここからは、ゲームの主要機能を一つずつ実装していきます。

1. プロジェクト管理システム

プロジェクト管理は経営シミュレーションの核となる部分です。プロジェクトの受注から完了までの流れを実装します。


// プロジェクトリストの更新
function updateProjectsList(projects) {
  projectsListElement.innerHTML = '';
  
  if (projects.length === 0) {
    projectsListElement.innerHTML = '現在進行中のプロジェクトはありません';
    return;
  }
  
  for (const project of projects) {
    const projectElement = document.createElement('li');
    projectElement.className = 'project-item';
    
    const statusClass = project.completed ? 'completed' : 
                       (project.daysLeft 
        ${project.name}
        ${project.client}
      
      
        予算: ${project.budget.toLocaleString()}
        残り日数: ${project.daysLeft}
        難易度: ${getDifficultyText(project.difficulty)}
        担当者: ${getAssignedStaffNames(project.assignedStaff)}
      
      
        ${project.completed ? 
          '完了' : 
          `スタッフ割当
           納品する`
        }
      
    `;
    
    projectsListElement.appendChild(projectElement);
  }
  
  // イベントリスナーの追加
  document.querySelectorAll('.assign-staff-button').forEach(button => {
    button.addEventListener('click', function() {
      const projectId = parseInt(this.getAttribute('data-project-id'));
      showAssignStaffDialog(projectId);
    });
  });
}

2. スタッフ管理システム

会社の成長には優秀なスタッフが欠かせません。スタッフの雇用、育成、管理機能を実装します。

// スタッフを雇用
function hireStaff(staffType) {
  const staffTypes = {
    "デザイナー": {
      baseSalary: 250000,
      skills: { design: 70, coding: 20, management: 30, communication: 50 },
      hireCost: 100000
    },
    "エンジニア": {
      baseSalary: 300000,
      skills: { design: 30, coding: 80, management: 40, communication: 40 },
      hireCost: 150000
    },
    "ディレクター": {
      baseSalary: 350000,
      skills: { design: 40, coding: 30, management: 70, communication: 60 },
      hireCost: 200000
    }
  };
  
  const template = staffTypes[staffType];
  if (!template) return { success: false, message: "指定されたスタッフタイプが見つかりません。" };
  
  if (game.company.funds  s.id === staffId);
    if (staff) {
      // プロジェクトに必要なスキルの平均値を計算
      let skillSum = 0;
      let skillCount = 0;
      
      for (const skill of project.requiredSkills) {
        if (staff.skills[skill]) {
          skillSum += staff.skills[skill];
          skillCount++;
        }
      }
      
      if (skillCount > 0) {
        const avgSkill = skillSum / skillCount;
        // スタッフのスキルの影響を大きくする
        quality += (avgSkill / assignedStaff.length) * 1.5;
      }
      
      // モチベーションの影響も大きくする
      quality += (staff.motivation - 50) / 5;
    }
  }
  
  // 設備の影響(新たに追加)
  const relevantEquipment = game.company.equipment.filter(eq => 
    eq.name.includes("PC") || 
    (project.requiredSkills.includes("design") && eq.name.includes("タブレット"))
  );
  
  if (relevantEquipment.length > 0) {
    const avgEquipmentPerformance = relevantEquipment.reduce((sum, eq) => sum + eq.performance, 0) / relevantEquipment.length;
    quality += (avgEquipmentPerformance - 50) / 4;
  } else {
    // 必要な機材がない場合はペナルティ
    quality -= 10;
  }
  
  // 納期影響(調整)
  if (project.daysLeft > project.duration * 0.3) {
    quality += 15; // 余裕があると品質アップ(より大きく)
  } else if (project.daysLeft < 0) {
    quality -= 25; // 納期遅れは大幅減点(より厳しく)
  }
  
  // 最終調整
  return Math.max(0, Math.min(100, Math.floor(quality)));
}

🎮 ゲームの完成と拡張

基本的なゲームシステムが完成したら、以下のような拡張機能を追加して、ゲームをより面白くすることができます:

  1. セーブ&ロード機能:LocalStorageを使ってゲーム状態を保存
  2. 実績システム:特定の条件を達成すると解除される実績
  3. ランダムイベント:予期せぬ出来事が発生するシステム
  4. 競合他社:AIが操作する競合会社との競争
  5. 特殊プロジェクト:稀に出現する高難度・高報酬のプロジェクト

例えば、セーブ&ロード機能は以下のように実装できます:


// ゲームの状態を保存
function saveGame() {
  const saveData = {
    company: game.company,
    gameTime: game.gameTime,
    version: "1.0.0" // バージョン管理用
  };
  
  localStorage.setItem('webDevTycoonSave', JSON.stringify(saveData));
  return { success: true, message: "ゲームを保存しました。" };
}

// ゲームの状態を読み込み
function loadGame() {
  const saveData = localStorage.getItem('webDevTycoonSave');
  if (!saveData) return { success: false, message: "セーブデータが見つかりません。" };
  
  try {
    const parsedData = JSON.parse(saveData);
    
    // バージョンチェック(将来的な互換性のため)
    if (parsedData.version !== "1.0.0") {
      return { success: false, message: "互換性のないセーブデータです。" };
    }
// データの復元
    game.company = parsedData.company;
    game.gameTime = parsedData.gameTime;
    
    updateUI();
    return { success: true, message: "ゲームを読み込みました。" };
  } catch (error) {
    return { success: false, message: "セーブデータの読み込みに失敗しました。" };
  }
}

📱 レスポンシブデザインの実装

現代のWebアプリケーションには、レスポンシブデザインが不可欠です。様々な画面サイズに対応するためのCSSを追加しましょう。

/* レスポンシブデザイン */
@media (max-width: 1024px) {
  .game-container {
    grid-template-columns: 1fr;
  }
  
  .game-header {
    grid-column: 1;
  }
  
  .side-panel {
    grid-column: 1;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
  
  .character-area {
    display: flex;
    align-items: center;
  }
  
  .character-image {
    width: 80px;
    height: 80px;
    margin-right: 15px;
  }
  
  .game-main {
    grid-column: 1;
  }
  
  .game-content {
    grid-template-columns: 1fr;
  }
  
  .projects-section,
  .staff-section,
  .equipment-section {
    grid-column: 1;
  }
}

@media (max-width: 600px) {
  .game-header {
    flex-direction: column;
    align-items: flex-start;
  }
  
  .company-stats {
    margin-top: 10px;
  }
  
  .game-date {
    margin-top: 10px;
    align-self: flex-end;
  }
  
  .action-buttons {
    flex-direction: column;
    gap: 5px;
  }
  
  .project-details,
  .staff-details {
    grid-template-columns: 1fr;
  }
}

🔍 デバッグとテスト

ゲーム開発では、デバッグとテストが非常に重要です。以下のようなテスト関数を作成して、ゲームの各機能をテストしましょう。

// テスト用関数
function runGameTests() {
  console.log("=== ゲームテスト開始 ===");
  
  // 1. 日付進行テスト
  console.log("1. 日付進行テスト");
  const initialDate = { ...game.gameTime };
  game.advanceDay();
  console.assert(
    game.gameTime.day === initialDate.day + 1 || 
    (initialDate.day === 30 && game.gameTime.day === 1),
    "日付進行に問題があります"
  );
  
  // 2. プロジェクト生成テスト
  console.log("2. プロジェクト生成テスト");
  const newProject = game.generateNewProject();
  console.assert(
    newProject && newProject.name && newProject.budget > 0,
    "プロジェクト生成に問題があります"
  );
  
  // 3. プロジェクト受注テスト
  console.log("3. プロジェクト受注テスト");
  const initialFunds = game.company.funds;
  const initialProjectCount = game.company.projects.length;
  const acceptResult = game.acceptProject(newProject);
  console.assert(
    acceptResult.success && 
    game.company.funds === initialFunds + newProject.advancePayment &&
    game.company.projects.length === initialProjectCount + 1,
    "プロジェクト受注に問題があります"
  );
  
  // 4. スタッフ割り当てテスト
  console.log("4. スタッフ割り当てテスト");
  const projectId = game.company.projects.id;
  const staffId = game.company.staff.id;
  const assignResult = game.assignStaffToProject(projectId, staffId);
  console.assert(
    assignResult.success && 
    game.company.projects.assignedStaff.includes(staffId),
    "スタッフ割り当てに問題があります"
  );
  
  // 5. プロジェクト完了テスト
  console.log("5. プロジェクト完了テスト");
  const projectToComplete = game.company.projects;
  projectToComplete.daysLeft = 0; // 強制的に納期を到来させる
  const completeResult = game.completeProject(projectToComplete.id);
  console.assert(
    completeResult.success && 
    projectToComplete.completed,
    "プロジェクト完了に問題があります"
  );
  
  console.log("=== テスト完了 ===");
}

🚀 パフォーマンス最適化

ブラウザゲームでは、パフォーマンスも重要な要素です。特に、UIの更新処理は効率的に行う必要があります。


// パフォーマンス最適化版のUI更新
function optimizedUpdateUI() {
  // 変更があった部分だけを更新する
  
  // 1. 基本情報の更新(変更があった場合のみ)
  if (companyNameElement.textContent !== game.company.name) {
    companyNameElement.textContent = game.company.name;
  }
  
  const formattedFunds = game.company.funds.toLocaleString() + '';
  if (fundsElement.textContent !== formattedFunds) {
    fundsElement.textContent = formattedFunds;
  }
  
  if (reputationElement.textContent != game.company.reputation) {
    reputationElement.textContent = game.company.reputation;
  }
  
  const formattedDate = `${game.gameTime.year}${game.gameTime.month}${game.gameTime.day}日`;
  if (dateElement.textContent !== formattedDate) {
    dateElement.textContent = formattedDate;
  }
  
  // 2. リストの更新(差分だけ更新)
  updateProjectsListOptimized(game.company.projects);
  updateStaffListOptimized(game.company.staff);
  updateEquipmentListOptimized(game.company.equipment);
}

// プロジェクトリストの最適化更新
function updateProjectsListOptimized(projects) {
  // 既存のプロジェクト要素を取得
  const existingProjects = Array.from(projectsListElement.children).filter(
    el => el.classList.contains('project-item')
  );
  
  // 空リストの表示/非表示を切り替え
  const emptyListElement = projectsListElement.querySelector('.empty-list');
  if (projects.length === 0) {
    if (!emptyListElement) {
      const emptyEl = document.createElement('li');
      emptyEl.className = 'empty-list';
      emptyEl.textContent = '現在進行中のプロジェクトはありません';
      projectsListElement.appendChild(emptyEl);
    }
    // 既存のプロジェクト要素をすべて削除
    existingProjects.forEach(el => el.remove());
    return;
  } else if (emptyListElement) {
    emptyListElement.remove();
  }
  
  // 既存のプロジェクトIDを取得
  const existingProjectIds = existingProjects.map(
    el => parseInt(el.querySelector('button')?.getAttribute('data-project-id') || '0')
  );
  
  // 新しいプロジェクトを追加
  for (const project of projects) {
    const existingIndex = existingProjectIds.indexOf(project.id);
    
    if (existingIndex === -1) {
      // 新規プロジェクトの場合、要素を作成して追加
      const projectElement = createProjectElement(project);
      projectsListElement.appendChild(projectElement);
    } else {
      // 既存プロジェクトの場合、内容を更新
      updateExistingProjectElement(existingProjects[existingIndex], project);
    }
  }
  
  // 削除されたプロジェクトの要素を削除
  const currentProjectIds = projects.map(p => p.id);
  existingProjects.forEach((el, index) => {
    const projectId = existingProjectIds[index];
    if (!currentProjectIds.includes(projectId)) {
      el.remove();
    }
  });
}

🎯 実践的なゲーム開発のポイント

最後に、ブラウザゲーム開発で重要なポイントをいくつか紹介します:

1. 状態管理の設計

ゲームの状態管理は非常に重要です。今回は単純なオブジェクトで管理しましたが、規模が大きくなる場合は以下の方法も検討してください:

  • Reduxパターン:状態の一元管理と予測可能な更新
  • イミュータブルデータ構造:状態の変更を追跡しやすくする
  • オブザーバーパターン:状態変更を監視して自動的にUIを更新

2. モジュール化と再利用性

コードの再利用性を高めるために、機能ごとにモジュール化することが重要です:

// モジュール化の例
const GameTimeModule = {
  advanceDay(gameTime, callbacks) {
    gameTime.day++;
    
    if (gameTime.day > 30) {
      gameTime.day = 1;
      gameTime.month++;
      
      if (callbacks.onMonthEnd) {
        callbacks.onMonthEnd();
      }
    }
    
    if (gameTime.month > 12) {
      gameTime.month = 1;
      gameTime.year++;
      
      if (callbacks.onYearEnd) {
        callbacks.onYearEnd();
      }
    }
    
    return gameTime;
  }
};

const ProjectModule = {
  generateNewProject(companyReputation, templates) {
    // プロジェクト生成ロジック
  },
  
  calculateQuality(project, assignedStaff, equipment) {
    // 品質計算ロジック
  }
};

3. ローカライゼーション対応

国際的なユーザーに対応するために、ローカライゼーション(多言語対応)を考慮することも重要です:

// 言語ファイルの例
const translations = {
  ja: {
    next_day: "次の日へ",
    hire_staff: "スタッフを雇用",
    buy_equipment: "機材を購入",
    upgrade_office: "オフィスをアップグレード",
    // ...
  },
  en: {
    next_day: "Next Day",
    hire_staff: "Hire Staff",
    buy_equipment: "Buy Equipment",
    upgrade_office: "Upgrade Office",
    // ...
  }
};

// 言語切り替え関数
function setLanguage(lang) {
  const texts = translations[lang] || translations.en;
  
  // UI要素のテキストを更新
  document.getElementById('next-day-button').textContent = texts.next_day;
  document.getElementById('hire-staff-button').textContent = texts.hire_staff;
  document.getElementById('buy-equipment-button').textContent = texts.buy_equipment;
  document.getElementById('upgrade-office-button').textContent = texts.upgrade_office;
  // ...
}

📝 まとめ

今回は、JavaScriptを使ったブラウザゲーム開発の基本を解説しました。経営シミュレーションゲームは、オブジェクト指向プログラミングの練習に最適であり、状態管理やUI設計など、Webアプリケーション開発の多くの要素を学ぶことができます。

このゲームはシンプルな構造ですが、拡張性が高いため、様々な機能を追加してより複雑で面白いゲームに発展させることができます。例えば、マーケティング要素の追加、スタッフ間の相性システム、技術ツリーの実装など、アイデア次第で無限に拡張可能です。

スクリーンショット 2025-03-08 21.23.22.png

ぜひ、このコードをベースに自分だけのオリジナルゲームを作ってみてください!プログラミングの楽しさを実感できるはずです。

次回は、このゲームにアニメーションやサウンドを追加して、よりリッチな体験を提供する方法について解説する予定です。お楽しみに!

🔗 参考リソース


この記事が気に入ったら、ぜひコメントやシェアをお願いします!また、質問や感想も大歓迎です。一緒にゲーム開発の世界を楽しみましょう!


最後に:業務委託のご相談を承ります

私は、業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用し、レスポンシブなWebサイトやインタラクティブなアプリケーション、API連携など、幅広いニーズに対応可能です。

「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、ぜひお気軽にご相談ください。一緒にビジネスの成長を目指しましょう!

👉 ポートフォリオ

🌳 らくらくサイト

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?